#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

#include <linux/crypto.h>
#include <linux/err.h>
#include <crypto/compress.h>
#include <linux/netlink.h>

MODULE_LICENSE("Dual BSD/GPL");

static int major_num = 0;
static int logical_block_size = 512;
static int volume = 4;
static int nsectors;
module_param(volume, int, S_IRUGO);
static char * mode = "gradually";
module_param(mode, charp, S_IRUGO);

struct record {
	u8 *data;
	int size;
};

struct record_ext {
	struct record rec[3];
	int size;
};

#define KERNEL_SECTOR_SIZE 512

static struct request_queue *Queue;
static struct vhd_device {
	unsigned long size;
	struct record_ext *records;
	u8 *data;
	spinlock_t lock;
	struct gendisk *gd;
} Device;

const int block[5]={500, 244, 120, 56, 24};
int flag = 0;
int recordSizes[4];
int flag_mem = 0;

int get_sizes (int ind, int size, int available)
{
	int i;	
	int k;
	int rezult;
	int remain;
	int allocated;
	allocated = 0;
	for(i = ind; i<(sizeof(block)/sizeof(int));i++) {
		rezult = size / block[i];
		remain = size % block[i];
		if(rezult<available && remain!=0 && i!=(sizeof(block)/sizeof(int)-1)) {
			allocated = get_sizes(i+1, remain, available-rezult);
			if(!allocated) {
				rezult++;
				flag = 1;
			}
			else {
				if(allocated > (rezult + 1) * block[i])
				{
					rezult++;
					for(k=0; k<3; k++)
						recordSizes[i]=0;
					allocated = 0;					
					flag = 1;
				}
			}
		}
		else {
			if((rezult==available && remain != 0)||(rezult>available))
				return 0;
			if(rezult<available && remain!=0) {
				flag = 1;
				rezult++;
			}
			if(rezult<=available && remain == 0)
				flag = 1;
		}
		if(flag) {
			int j;
			int all;
			all = 0;
			for(j=0; j<rezult; j++) {
				for(k=0;k<3;k++)
					if(!recordSizes[k]) {
						recordSizes[k]=block[i];
						break;
					}
				all+=block[i];
			}
			recordSizes[3]=all+allocated;
			return all+allocated;
		}
	}
	return 0;
}

static void my_transfer(struct vhd_device *dev, sector_t sector,
		unsigned long nsect, char *buffer, int write) {
	unsigned long offset = sector * logical_block_size;
	unsigned long nbytes = nsect * logical_block_size;
	int i, cur_rec, j, step;
	i=0;

	if ((offset + nbytes) > dev->size) {
		printk (KERN_NOTICE "VHD :: Beyond-end write (Offset : %ld, Bytes for reading/writing : %ld)\n", offset, nbytes);
		return;
	}
	if(!strcmp(mode, "gradually"))
	{
		if (write) {
			for(cur_rec = sector; cur_rec < sector+nsect; cur_rec++, i++) {
				step = 0;
	
				for(j = 0; j < 4; j++)
					recordSizes[j] = 0;
				get_sizes(0, 512, 3);
	
				if (!dev->records[cur_rec].size) {
					for(j = 0; j < 3; j++) {
						if(!j)
							dev->records[cur_rec].rec[j].size = recordSizes[j]-(recordSizes[3]-512);
						if(j)
							dev->records[cur_rec].rec[j].size = recordSizes[j];
						if(dev->records[cur_rec].rec[j].size) {
							dev->records[cur_rec].rec[j].data = (u8 *) kmalloc(dev->records[cur_rec].rec[j].size, GFP_ATOMIC);
							if(dev->records[cur_rec].rec[j].data == NULL) {
								printk (KERN_NOTICE "VHD :: Cann't allocated %d byte memory \n", logical_block_size);
								return;
							}
						}
					}
					dev->records[cur_rec].size = recordSizes[3];
				}
				for(j = 0; j < 3; j++) {
					if(dev->records[cur_rec].rec[j].size) {
						if(j)
							step+=dev->records[cur_rec].rec[j-1].size;
						memcpy(dev->records[cur_rec].rec[j].data, buffer+i*logical_block_size+step, dev->records[cur_rec].rec[j].size);
					}
				}
			}
		}
		else {
			for(cur_rec = sector; cur_rec < sector+nsect; cur_rec++, i++) {
				step = 0;
				if (!dev->records[cur_rec].size)
					return;
				else {
					for (j = 0; j < 3; j++)
					{
						if(j)
							step+=dev->records[cur_rec].rec[j-1].size;
						memcpy(buffer+i*logical_block_size+step, dev->records[cur_rec].rec[j].data, dev->records[cur_rec].rec[j].size);
					}
				}
			}
		}
	}
	else
	{
		if (write) {
			memcpy(dev->data + offset, buffer, nbytes);
		}
		else {
			memcpy(buffer, dev->data + offset, nbytes);
		}
	
	}
}

static void my_request(struct request_queue *q) {
	struct request *req;

	req = blk_fetch_request(q);
	while (req != NULL) {
		if (!blk_fs_request(req)) {
			printk (KERN_NOTICE "VHD :: Skip non-File System request\n");
			__blk_end_request_all(req, -EIO);
			continue;
		}
		my_transfer(&Device, blk_rq_pos(req), blk_rq_cur_sectors(req),
				req->buffer, rq_data_dir(req));
		if ( ! __blk_end_request_cur(req, 0) ) {
			req = blk_fetch_request(q);
		}
	}
}

int vhd_getgeo(struct block_device * block_device, struct hd_geometry * geo) {
	long size;
	
	size = Device.size / KERNEL_SECTOR_SIZE;
	geo->cylinders = size >> 6;
	geo->heads = 4;
	geo->sectors = 16;
	geo->start = 0;
	return 0;
}

static struct block_device_operations operations = {
		.owner  = THIS_MODULE,
		.getgeo = vhd_getgeo,
};

static int __init vhd_init(void) {
	int i;
	int j;
	nsectors = volume * 2048;
	Device.size = nsectors * logical_block_size;
	spin_lock_init(&Device.lock);
	if(strcmp(mode, "gradually") && strcmp(mode, "immediately")) {
		printk(KERN_WARNING "Used an unexpected settings \n");
		return -1;
	}		
	if(!strcmp(mode, "gradually")) {
		Device.records = kmalloc(sizeof(struct record_ext) * nsectors, GFP_KERNEL);
		if (Device.records == NULL) {
			Device.records = vmalloc(sizeof(struct record_ext) * nsectors);
			flag_mem = 1;
			if (Device.records == NULL) {
				printk(KERN_WARNING "VHD :: Cann't allocated %d M bymemory \n", volume);
				return -ENOMEM;
			}
		}
		for (i = 0; i<nsectors; i++) {
			for(j = 0; j<3; j++)
				Device.records[i].rec[j].size = 0;
			Device.records[i].size = 0;
		}
	}
	else {
		printk(KERN_INFO "VHD :: Memmory allocated");
		Device.data = NULL;
		Device.data = (u8*) vmalloc (Device.size);
		if(Device.data == NULL) {
			printk(KERN_WARNING "VHD :: Cann't allocated %d M memory \n", volume);
			return -ENOMEM;
		}
	}
	Queue = blk_init_queue(my_request, &Device.lock);
	if (Queue == NULL)
		goto out;
	blk_queue_logical_block_size(Queue, logical_block_size);
	
	major_num = register_blkdev(major_num, "vhd");
	if (major_num <= 0) {
		printk(KERN_WARNING "Unable to get major number in vhd\n");
		goto out;
	}

	Device.gd = alloc_disk(16);
	if (!Device.gd)
		goto out_unregister;
	Device.gd->major = major_num;
	Device.gd->first_minor = 0;
	Device.gd->fops = &operations;
	Device.gd->private_data = &Device;
	strcpy(Device.gd->disk_name, "vhd0");
	set_capacity(Device.gd, nsectors);
	Device.gd->queue = Queue;
	add_disk(Device.gd);
	
	return 0;

out_unregister:
	unregister_blkdev(major_num, "vhd");
out:
	if(!strcmp(mode, "gradually"))
		kfree(Device.records);
	return -ENOMEM;
}

static void __exit vhd_exit(void)
{
	int i;
	int j;
	del_gendisk(Device.gd);
	put_disk(Device.gd);
	unregister_blkdev(major_num, "vhd");
	blk_cleanup_queue(Queue);
	if(!strcmp(mode, "gradually"))
	{
		for (i=0; i<nsectors; i++)
			if(Device.records[i].size)
				for (j=0; j<3; j++)
					if(Device.records[i].rec[j].size)
							kfree(Device.records[i].rec[j].data);
		if(!flag_mem)
			kfree(Device.records);
		else
			vfree(Device.records);
	}
	else
		vfree(Device.data);
}

module_init(vhd_init);
module_exit(vhd_exit);
